rix: environnements de développement reproductibles pour développeurs R

Bruno Rodrigues

Qui suis-je?

  • Bruno Rodrigues, responsable des départements “statistiques” et “stratégie de données” du Ministère de la Recherche et de l’Enseignement supérieur au Luxembourg

  • Utilisateur de R depuis 2009

  • Cette présentation est disponible sur le lien suivant https://is.gd/nix_russ

  • Code source disponible ici: https://github.com/b-rodrigues/russ_workshop

But de cet atelier

  • Apprendre juste ce qu’il faut de Nix pour “être dangereux”
  • Programme:
* C'est quoi Nix?
* Le problème que Nix résoud
* Le *gestionnaire de paquets fonctionnel*
* Le langage Nix
* rix, ou comment utiliser Nix facilement
  • Si le temps le permet: créer son propre cache de binaires avec Cachix

C’est quoi Nix ? (1/2)

  • Un gestionnaire de paquets
  • Un langage de programmation
  • Une distribution Linux (NixOS)

Le sujet d’aujourd’hui: le gestionnaire de paquets

C’est quoi Nix ? (2/2)

  • Nix est un outil complexe

  • Il faut de l’investissement de la part des utilisateurs

  • Mais {rix} va nous aider!

Le gestionnaire de paquets Nix

  • Gestionnaire de paquets: un outil pour administrer des …paquets

  • Paquet: n’importe quel logiciel (pas seulement des paquets R)

  • Voici un gestionnaire de paquets populaire:

Le gestionnaire de paquets Nix

Google Play Store

Le problème que Nix résoud (1/3)

  • Proposition de valeur de Nix:

Installe tous les logiciels nécessaires (R, paquets R, libraries de développement, etc) de manière totalement reproductible et sur n’importe quelle plateforme en écrivant une seule expression dans le langage Nix.

Le problème que Nix résoud (2/3)

  • Comment retrouver la même chose sans Nix?
  • Il faut figer R avec le R Installation Manager
  • Il faut figer les paquets R avec {renv}
  • Il faut figer toutes les autres dépendances invisibles, idéalement avec Docker

(Remarque: on peut s’abstraire de rig si Docker inclut la bonne version de R)

Nix permet de tout gérer d’un seul coup!

Le problème que Nix résoud (3/3)

  • Sans Nix (ou Docker + {renv}) on doit accepter les risques suivants:
* Collaborateurs travaillent sur des environnements hétérogènes
* Les analyses produites ne sont (très probablement) pas reproductibles

Le gestionnaire de paquets fonctionnel (1/3)

  • Nix est un gestionnaire de paquets fonctionnel

  • Fonctionnel, comme la programmation fonctionnelle inspirée du lambda-calcul

  • Lambda-calcul? Pour faire simple:

-> f(x)=y

  • f(x) va toujours donner y

  • Autrement dit, y ne dépend de rien d’autre que de x qui est transformé par f

Le gestionnaire de paquets fonctionnel (2/3)

The idea is to always deploy component closures: if we deploy a component, then we must also deploy its dependencies, their dependencies, and so on. That is, we must always deploy a set of components that is closed under the ‘’depends on’’ relation. Since closures are selfcontained, they are the units of complete software deployment. After all, if a set of components is not closed, it is not safe to deploy, since using them might cause other components to be referenced that are missing on the target system.

Eelco Dolstra, Nix: A Safe and Policy-Free System for Software Deployment

Le gestionnaire de paquets fonctionnel (3/3)

  • Par exemple: install.packages("dplyr") ne va pas toujours donner le même résultat!
  • Quelle est la variable cachée?

Le langage Nix (1/6)

let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/976fa3369d722e76f37c77493d99829540d43845.tar.gz") {};
  system_packages = builtins.attrValues {
    inherit (pkgs) R ;
  };
in
  pkgs.mkShell {
    buildInputs = [ system_packages ];
    shellHook = "R --vanilla";
  }

Le langage Nix (2/6)

  • Défini le dépôt à utiliser (avec un commit figé)
  • Liste les paquets à installer
  • Défini ce qui doit être construit: un shell

Le langage Nix (3/6)

  • Les paquets Nix sont tous téléchargés depuis un mono-dépôt gigantesque sur Github
  • Github: reproductibilité assurée via les commits!
  • Par exemple, ce commit installera R 4.3.1 et les paquets associés:
pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/976fa3369d722e76f37c77493d99829540d43845.tar.gz") {};

Le langage Nix (4/6)

  • system_packages: une variable qui liste les paquets à installer
  • Ici, seulement R:
system_packages = builtins.attrValues {
  inherit (pkgs) R ;
};

Le langage Nix (5/6)

  • Finalement, on définit un shell:
pkgs.mkShell {
  buildInputs = [ system_packages ];
  shellHook = "R --vanilla";
}
  • Ce shell inclura les paquets définis dans system_packages (buildInputs)
  • Et va lancer R --vanilla au démarrage (shellHook)

Le langage Nix (6/6)

  • Écrire ces expressions nécessite l’apprentissage d’une nouveau langage
  • Un langage puissant certes… mais si tout ce qu’on veut c’est des environnements de développement reproductibles…
  • …alors {rix} est la solution!

Nix expressions

  • Nix expressions can be used to install software
  • But we will use them to build per-project development shells
  • We will include R, LaTeX packages, or Quarto, Python, Julia….
  • Nix takes care of installing every dependency down to the compiler!

CRAN and Bioconductor

  • CRAN is the repository of R packages to extend the language
  • As of writing, +20000 packages available
  • Biocondcutor: repository with a focus on Bioinformatics: +2000 more packages
  • Almost all available through nixpkgs in the rPackages set!
  • Find packages here

rix: reproducible development environments with Nix (1/4)

  • {rix} (website) makes writing Nix expression easy!
  • Simply use the provided rix() function:
library(rix)

rix(r_ver = "4.3.1",
    r_pkgs = c("dplyr", "ggplot2"),
    system_pkgs = NULL,
    git_pkgs = NULL,
    tex_pkgs = NULL,
    ide = "rstudio",
    # This shellHook is required to run Rstudio on Linux
    # you can ignore it on other systems
    shell_hook = "export QT_XCB_GL_INTEGRATION=none",
    project_path = ".")

rix: reproducible development environments with Nix (2/4)

  • List required R version and packages
  • Optionally: more system packages, packages hosted on Github, or LaTeX packages
  • Optionally: an IDE (Rstudio, Radian, VS Code or “other”)
  • Work interactively in an isolated environment!

rix: reproducible development environments with Nix (3/4)

  • rix::rix() generates a default.nix file
  • Build expressions using nix-build (in terminal) or rix::nix_build() from R
  • “Drop” into the development environment using nix-shell
  • Expressions can be generated even without Nix installed

rix: reproducible development environments with Nix (4/4)

  • Can install specific versions of packages (write "dplyr@1.0.0")
  • Can install packages hosted on Github
  • Many vignettes to get you started! See here

Let’s check out expressions/rix_intro/

Non-interactive use

  • {rix} makes it easy to run pipelines in the right environment
  • (Little side note: the best tool to build pipelines in R is {targets})
  • See expressions/nix_targets_pipeline
  • Can also run the pipeline like so:
cd /absolute/path/to/pipeline/ && nix-shell default.nix --run "Rscript -e 'targets::tar_make()'"

Nix and Github Actions: running pipelines

  • Possible to easily run a {targets} pipeline on Github actions
  • Simply run rix::tar_nix_ga() to generate the required files
  • Commit and push, and watch the actions run!
  • See here.

Nix and Github Actions: writing papers

  • Easy collaboration on papers as well
  • See here
  • Just focus on writing!

Subshells

  • Also possible to evaluate single functions inside a “subshell”
  • Works from R installed via Nix or not!
  • Very useful to use hard-to-install packages such as {arrow}
  • See expressions/subshell

R packages release cycle

  • CRAN is updated daily, but it’s not reflected in nixpkgs
  • The rPackages set gets updated around new R releases (every 3 months or so)
  • What if more recent packages are required?
  • One solution: use our nixpkgs fork from our rstats-on-nix organisation!

Bleeding edge development environments (1/2)

  • To use our bleeding edge fork simply change the pkgs variable:
pkgs = import (fetchTarball "https://github.com/rstats-on-nix/nixpkgs/archive/refs/heads/r-daily.tar.gz") {};
  • You can also use a specific commit of the fork instead:
pkgs = import (fetchTarball "https://github.com/rstats-on-nix/nixpkgs/archive/78f705bd8689ad7d215f4b3aea9d9c1302a31b99.tar.gz") {};
  • Will soon be supported in {rix}

Bleeding edge development environments (2/2)

  • But: everything needs to be built from source
  • Solution: roll out your own binary cache using Cachix
  • Build the development environment on Github Actions
  • On your pc: install pre-compiled binaries!

To know more